Skip to content

fix(sdk): generate titles with Responses and subscription streaming - #4968

Merged
neubig merged 3 commits into
mainfrom
fix/subscription-title-generation
Sep 12, 2026
Merged

neubig merged 3 commits into
mainfrom
fix/subscription-title-generation

Conversation

@neubig

@neubig neubig commented Sep 12, 2026

Copy link
Copy Markdown
Member

HUMAN:

User request, quoted verbatim from @neubig: “OK, create the new PRs and stack 3403 on top of the conversation-scoped API one.” Follow-up: “OK, perform that separation. And update the PR.” Later follow-up: “Don’t we have a make_llm_completion function that could be used here instead?” Clarification: remove make_llm_completion / amake_llm_completion, migrate callers to generic LLM dispatch, and keep agent-only policy out of generic generation.

AGENT:

Why

Responses-capable and subscription models need correct API dispatch and streaming behavior for title generation and other auxiliary LLM calls. API-mode selection belongs in the generic LLM abstraction; agent response policy should remain explicit at the agent call site.

Summary

  • Add LLM.generate() and LLM.agenerate() to select Responses or Chat Completions from configured API mode.
  • Generate conversation titles through LLM.generate(store=False), including mandatory subscription streaming.
  • Remove make_llm_completion() and amake_llm_completion().
  • Migrate agent steps, condensation, hook evaluation, cleanup, vision inspection, Ask Oracle, and conversation questions to direct generate() / agenerate() calls.
  • Keep tools, store=False, callbacks, call context, and add_security_risk_prediction=True explicit only on actual agent-response calls.
  • Replace stacked-base dynamic attribute calls instead of adding allowances, shrinking the baseline from 131 to 122 entries.
  • Treat obj.__dict__.get(...) as forbidden dynamic attribute access while allowing ordinary mapping .get(...).
  • Read LiteLLM cache accounting through its normalized, typed Usage and ResponseAPIUsage detail fields instead of dynamic/private storage.
  • Enforce in CI that scripts/forbidden_dynamic_attributes_baseline.json may only lose entries, never add them, relative to the PR base or previous main commit.

How to Test

  • uv run pytest -q tests/sdk/llm/test_llm.py tests/sdk/agent/test_agent_utils.py tests/sdk/agent/test_agent_step_responses_gating.py tests/sdk/agent/test_message_during_streaming_arun.py tests/sdk/agent/test_non_multimodal_image_input.py tests/sdk/context/condenser/test_llm_summarizing_condenser.py tests/sdk/conversation/test_generate_title.py tests/sdk/llm/test_cleanup_profile.py tests/sdk/hooks/test_executor.py tests/sdk/hooks/test_integration.py tests/tools/ask_oracle/test_ask_oracle.py — 257 passed.
  • uv run pytest -q tests/agent_server/test_profiles_router.py -k 'preflight or validate or subscription' — 12 passed.
  • uv run pytest -q tests/cross/test_check_forbidden_dynamic_attributes.py tests/sdk/agent/test_stream_context.py tests/sdk/llm/test_llm_telemetry.py tests/sdk/llm/test_llm_span_cost.py — 83 passed.
  • uv run python scripts/check_forbidden_dynamic_attributes.py --baseline-ref origin/fix/async-secret-masking — passed; current baseline is a strict subset.
  • uv run pre-commit run --all-files --show-diff-on-failure — all hooks passed.
  • Full local SDK execution reached 6,184 passed, 7 skipped, and 12 xfailed; four failures were caused by this Agent Canvas environment’s injected user memory, /tmp being inside a Git repository, local user-agent lookup, and a WebSocket readiness environment override. The same affected change-specific tests pass in isolation.

Scope

The independent diff contains generic LLM dispatch, title-generation transport fixes, direct caller migration, wrapper removal, focused policy/dispatch tests, and stacked-base pre-commit remediation.

Issue Number

Split from existing PR #3403 at the author’s request; no separate issue was created.

This PR description was updated by an AI agent (OpenHands) on behalf of @neubig.


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:5bb59cc-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-5bb59cc-python \
  ghcr.io/openhands/agent-server:5bb59cc-python

All tags pushed for this build

ghcr.io/openhands/agent-server:5bb59cc-golang-amd64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-golang-amd64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-golang-amd64
ghcr.io/openhands/agent-server:5bb59cc-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:5bb59cc-golang-arm64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-golang-arm64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-golang-arm64
ghcr.io/openhands/agent-server:5bb59cc-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:5bb59cc-java-amd64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-java-amd64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-java-amd64
ghcr.io/openhands/agent-server:5bb59cc-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:5bb59cc-java-arm64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-java-arm64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-java-arm64
ghcr.io/openhands/agent-server:5bb59cc-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:5bb59cc-python-amd64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-python-amd64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-python-amd64
ghcr.io/openhands/agent-server:5bb59cc-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:5bb59cc-python-arm64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-python-arm64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-python-arm64
ghcr.io/openhands/agent-server:5bb59cc-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:5bb59cc-python-slim-amd64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-python-slim-amd64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-python-slim-amd64
ghcr.io/openhands/agent-server:5bb59cc-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim-amd64
ghcr.io/openhands/agent-server:5bb59cc-python-slim-arm64
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-python-slim-arm64
ghcr.io/openhands/agent-server:fix-subscription-title-generation-python-slim-arm64
ghcr.io/openhands/agent-server:5bb59cc-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim-arm64
ghcr.io/openhands/agent-server:5bb59cc-golang
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-golang
ghcr.io/openhands/agent-server:fix-subscription-title-generation-golang
ghcr.io/openhands/agent-server:5bb59cc-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:5bb59cc-java
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-java
ghcr.io/openhands/agent-server:fix-subscription-title-generation-java
ghcr.io/openhands/agent-server:5bb59cc-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:5bb59cc-python-slim
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-python-slim
ghcr.io/openhands/agent-server:fix-subscription-title-generation-python-slim
ghcr.io/openhands/agent-server:5bb59cc-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim
ghcr.io/openhands/agent-server:5bb59cc-python
ghcr.io/openhands/agent-server:5bb59ccb3aaf33aa24d78f94bbab396119ca1c5b-python
ghcr.io/openhands/agent-server:fix-subscription-title-generation-python
ghcr.io/openhands/agent-server:5bb59cc-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 5bb59cc-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 5bb59cc-python-amd64) are also available if needed

@all-hands-bot

Copy link
Copy Markdown
Collaborator

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   profiles_router.py196697%492–497
openhands-sdk/openhands/sdk/agent
   agent.py4503692%109, 291, 430, 434, 737–738, 745–746, 865, 869–870, 875–877, 879, 898–899, 905, 910, 945–946, 953–954, 999, 1006, 1020–1021, 1035–1036, 1287, 1372–1373, 1375, 1405, 1454, 1461
   stream_context.py145199%229
   utils.py2291892%93–95, 100–101, 167, 184–185, 320, 322, 324, 330, 340, 430, 503, 660–661, 666
openhands-sdk/openhands/sdk/context/condenser
   llm_summarizing_condenser.py1822984%339, 342–343, 346–347, 352, 357, 359–360, 373–374, 450–451, 456, 464, 480–481, 483–485, 490–494, 497, 501, 503–504
openhands-sdk/openhands/sdk/conversation
   title_utils.py58297%35, 83
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py10988792%168, 346, 412, 679–680, 722, 764, 1092, 1116–1117, 1122, 1137, 1139, 1243, 1259, 1307, 1333, 1413, 1417–1423, 1488, 1508–1510, 1559, 1578–1580, 1891–1892, 2130, 2133–2134, 2159, 2190, 2196, 2331, 2335, 2338, 2341, 2345–2346, 2350–2351, 2354, 2361, 2381, 2385, 2388, 2407, 2459, 2462, 2494, 2501–2502, 2510, 2514–2516, 2523, 2560–2566, 2569, 2572, 2579, 2680, 2685, 2805–2806, 2824–2825, 2858, 3064, 3068, 3138, 3145–3146
openhands-sdk/openhands/sdk/hooks
   executor.py2612491%67, 120–122, 127–134, 480, 490, 522–523, 535–536, 586–587, 601–602, 607–608
openhands-sdk/openhands/sdk/llm
   cleanup_profile.py48492%79–81, 146
   llm.py10738992%705, 744–745, 750, 860, 876, 1078, 1118–1120, 1154, 1161, 1311, 1439, 1710, 1714–1715, 1897, 1904, 1915, 1983, 1987–1988, 2057, 2064, 2074, 2081, 2092, 2140, 2153, 2171, 2185, 2187, 2189, 2214, 2216, 2225–2226, 2250, 2395, 2551–2552, 2665, 2978–2979, 2988, 3006, 3033–3034, 3036, 3038, 3040, 3048, 3051, 3053, 3055, 3066–3067, 3075, 3078, 3081–3082, 3093–3095, 3099, 3103–3104, 3109, 3119, 3124, 3188, 3190, 3192–3195, 3197–3200, 3205–3208, 3223, 3234, 3294, 3296
openhands-sdk/openhands/sdk/llm/utils
   telemetry.py2221991%143, 168, 174–175, 185, 197–198, 283–285, 308–309, 320–321, 429, 431, 445, 451, 456
openhands-sdk/openhands/sdk/tool/builtins
   vision_inspect.py1212282%55, 83, 103, 106–107, 109–110, 113–114, 116, 127, 143, 156, 168, 175, 188, 204–205, 217, 259, 305, 309
openhands-tools/openhands/tools/ask_oracle
   impl.py302130%50–51, 56–57, 61–62, 69–70, 74–75, 80, 85, 89, 97–100, 108, 113–114, 119
TOTAL43633838281% 

@all-hands-bot

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: e371ac842a8e15e15b73644f0865e3d6efa48408
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/a8019020-ecfc-4805-b78c-98974321659e

This comment was posted by an AI agent (OpenHands).

@all-hands-bot

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: e371ac842a8e15e15b73644f0865e3d6efa48408
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/95ee6c1b-3042-4993-ab5d-0f4141d24fa3

This comment was posted by an AI agent (OpenHands).

all-hands-bot
all-hands-bot previously approved these changes Sep 12, 2026

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Review

🟢 Good taste — clean, minimal, well-tested.

The PR makes two focused changes:

  1. Title generation API dispatch (title_utils.py): Routes Responses-capable models to llm.responses(messages, store=False) and others to llm.completion(messages). The streaming guard if llm.stream and not llm.requires_streaming correctly preserves mandatory streaming for subscription models (which reject non-streaming requests) while disabling it for regular streaming models that have no on_token callback wired during title generation. The store=False parameter is appropriate — title generation should not persist server-side.

  2. API breakage checker (check_sdk_api_breakage.py): Converts _ACCEPTED_REMOVED_MEMBERS from a frozenset to a dict with per-member reason strings, and adds LLM.modify_params as an accepted removal (removed in PR #4954 after its deprecation runway). This is a clean improvement — the diagnostic print now uses the per-member reason instead of a hardcoded message for all entries.

Verification

  • All 77 tests pass (12 title-generation + 65 breakage-checker).
  • The new test_title_uses_real_http_transport test is excellent: it spins up a loopback HTTP server and exercises all three modes (chat, responses, subscription) without mocking the LLM transport, verifying both API endpoint dispatch and streaming behavior.
  • The breakage test correctly verifies exact membership, negative cases (wrong feature, wrong package), and output content.

Risk Assessment

🟢 LOW — Title generation is a background cosmetic operation that does not affect agent reasoning, tool use, planning, memory, or terminal handling. The same prompt goes to the same model; only the API endpoint path changes for Responses-capable models. Not in the eval-risk category.

Verdict: ✅ Worth merging

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Summary

This PR makes two changes:

  1. Title generation dispatch (title_utils.py): Routes Responses-capable models to llm.responses(messages, store=False) instead of llm.completion(), and preserves streaming for subscription models that requires_streaming.
  2. CI tooling (check_sdk_api_breakage.py): Adds LLM.modify_params to the accepted-removals list and converts the data structure from frozenset to dict to carry per-member acceptance reasons.

Analysis

Title generation (title_utils.py)

The change correctly handles three cases:

  • Chat completions models: uses_responses_api() returns False, so completion() is used. Streaming is disabled via model_copy when not requires_streaming (same as before).
  • Responses-capable non-subscription models: uses_responses_api() returns True, so responses(store=False) is called. Streaming is disabled first (since requires_streaming is False), and responses() also has an internal fallback that disables streaming when on_token is None — double-safe.
  • Subscription models: requires_streaming is True, so streaming is NOT disabled. responses(store=False) is called with stream=True from self.stream, and the stream is drained internally by the _one_attempt logic without needing an on_token callback. This is correct — subscription endpoints reject non-streaming requests.

Both completion() and responses() return LLMResponse with a normalized Message, so the title extraction code (response.message.content[0]) works identically for both paths. Verified via _build_responses_result which calls Message.from_llm_responses_output().

The store=False parameter is appropriate for a stateless title-generation call. For subscription models, create_llm already sets litellm_extra_body={"store": False}, and select_responses_options sets out["store"] = False from the explicit parameter — no conflict since both are False.

CI tooling (check_sdk_api_breakage.py)

Clean refactor: frozenset to dict to allow per-member acceptance reason strings. The _is_accepted_removed_member lookup ((package, feature) in _ACCEPTED_REMOVED_MEMBERS) works identically for both types. The diagnostic message now pulls the reason from the dict value instead of hardcoding it. The new test verifies exact matching (rejects wrong package, wrong member name) and checks the diagnostic output contains "PR #4954".

Tests

The new test_title_uses_real_http_transport test is well-structured — it uses a real loopback HTTP server (no mocked LLM transport) and parametrizes across all three modes (chat, responses, subscription). Assertions verify the correct endpoint path, streaming behavior, and store=False for responses/subscription modes.

Risk Assessment

🟢 LOW — This is a focused bugfix to background title-generation utility code (not the agent reasoning loop, tool calling, planning, or condenser behavior). The CI tooling change is a data-structure refactor with exact-match tests. No breaking changes to public APIs, no event type modifications, no security concerns.

The title-generation path is a side-channel call that runs after the first user message; it does not affect the agent's core reasoning loop or benchmark/eval performance. However, since the repo review guidelines are strict about anything adjacent to agent behavior, I'm leaving a COMMENT rather than APPROVE — a human maintainer should make the final call on whether lightweight eval confirmation is needed.

Verdict: Code is correct and clean. No material issues found.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

GITHUB_REVIEW_POSTED

This comment was posted by an AI agent (OpenHands).

@all-hands-bot
all-hands-bot dismissed their stale review September 12, 2026 00:46

Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

GITHUB_REVIEW_POSTED

This comment was posted by an AI agent (OpenHands).

1 similar comment
@all-hands-bot

Copy link
Copy Markdown
Collaborator

GITHUB_REVIEW_POSTED

This comment was posted by an AI agent (OpenHands).

@neubig
neubig requested review from all-hands-bot and removed request for all-hands-bot September 12, 2026 00:56
@neubig
neubig force-pushed the fix/subscription-title-generation branch from e371ac8 to 0130668 Compare September 12, 2026 03:17
@neubig
neubig changed the base branch from main to fix/async-secret-masking September 12, 2026 03:18
@neubig
neubig added this pull request to stack #4972 September 12, 2026 03:18
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: 013066827a50c45f8bf113517cf7dd290d126ee7
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/14b011f5-931d-4b49-b4ba-48ea765bfcb3

This comment was posted by an AI agent (OpenHands).

all-hands-bot
all-hands-bot previously approved these changes Sep 12, 2026

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

🟢 Good taste — minimal, focused change that correctly dispatches title generation through the right API path for each model type.

Summary

The PR makes two changes to generate_title_with_llm:

  1. Streaming guard now exempts requires_streaming models — subscription models reject non-streaming requests, so the previous unconditional stream=False would break them. The responses() method already has the same requires_streaming exemption internally (llm.py:1763), so this is consistent.

  2. Responses API dispatch — Responses-capable models now use llm.responses(messages, store=False) instead of llm.completion(messages), with store=False to avoid persisting transient title-generation calls server-side.

Cross-file verification

  • LLM.responses() (llm.py:1762) has the same streaming guard as completion() but with the requires_streaming exemption, so subscription models correctly stay in streaming mode with internal drain. ✓
  • LLM.completion() (llm.py:1533) does not have the requires_streaming exemption — it unconditionally degrades to non-streaming when no on_token is provided. This is pre-existing and not introduced by this PR. In practice, subscription models use the Responses API, so this path isn't hit for them.
  • uses_responses_api() (llm.py:2530) respects explicit api_mode and falls back to model-feature capability detection for auto mode. ✓

Testing

The new test_title_uses_real_http_transport test is well-structured — it uses a real loopback HTTP server instead of mocked LLM transport, covering all three modes (chat, responses, subscription) and asserting on the actual request path, streaming flag, and store=False. This is a real code-path test that would catch regressions.

Risk Assessment

🟢 LOW — Title generation is a cosmetic side-channel that doesn't affect the agent reasoning loop, tool execution, or any benchmark-relevant behavior. No eval evidence needed.

Verdict: ✅ Worth merging — clean, correct, well-tested.

@all-hands-bot
all-hands-bot dismissed their stale review September 12, 2026 03:37

Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

GITHUB_REVIEW_POSTED

The review has been successfully posted to GitHub PR #4968 as an APPROVE review (review ID: 5185024480).

Summary of findings:

  • The PR correctly dispatches title generation through llm.responses(store=False) for Responses-capable models and llm.completion() otherwise
  • The requires_streaming exemption properly preserves mandatory streaming for subscription models, consistent with the internal guard in LLM.responses()
  • The new test uses a real loopback HTTP server (not mocks) covering all three modes (chat, responses, subscription)
  • Risk is LOW — title generation is a cosmetic side-channel that doesn't affect agent reasoning or benchmark behavior
  • No material issues found

This comment was posted by an AI agent (OpenHands).

@neubig
neubig force-pushed the fix/async-secret-masking branch from 1a84afa to 8cef037 Compare September 12, 2026 03:46
@neubig
neubig force-pushed the fix/subscription-title-generation branch from 0130668 to 8110e26 Compare September 12, 2026 03:46
@all-hands-bot

all-hands-bot commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

I’m not sure why streaming was disabled. I guess the question is, does it work now? For ChatGPT subscription.

@neubig
neubig force-pushed the fix/subscription-title-generation branch from d7d123b to f5b4a1e Compare September 12, 2026 14:45
@neubig
neubig force-pushed the fix/subscription-title-generation branch from f5b4a1e to 4ae12e8 Compare September 12, 2026 15:20
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: 4ae12e85ce18f1ae1fd2d215026e9aef3fa3a70f
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/2eb4d5c3-0301-4a62-9cef-49b6e2e26414

This comment was posted by an AI agent (OpenHands).

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Summary

Clean refactoring that moves Responses/Chat-Completions dispatch from make_llm_completion/amake_llm_completion utility functions into LLM.generate()/LLM.agenerate() methods on the LLM class itself. All callers migrated to direct generate()/agenerate() calls. Agent-only policy (add_security_risk_prediction=True, store=False) is now explicit at the agent call site rather than baked into a shared wrapper. Title generation no longer force-disables streaming, which correctly enables mandatory streaming for subscription models.

Analysis

Design: Good taste — the dispatch logic belongs on LLM (which owns uses_responses_api()), not in a utility function in agent/utils.py. The generate/agenerate methods are thin dispatch wrappers with **kwargs forwarding, matching the existing completion/responses signatures. The separation of agent-only add_security_risk_prediction from generic generation is the right call.

Behavioral changes verified as safe:

  • tools=None (new) vs tools=[] (old make_llm_completion default): both are falsy, so completion's if tools: and responses's if tools else None produce identical behavior.
  • Title generation streaming: old code did model_copy(update={"stream": False}) + completion(). New code calls generate() which lets completion/responses handle the streaming fallback gracefully (on_token=None -> non-streaming), except for requires_streaming models (subscription) where streaming is mandatory. The real HTTP transport test (test_title_uses_real_http_transport) covers all three modes.
  • ask_agent no longer passes add_security_risk_prediction=True: correct — it is a sidebar question, not an agent tool-calling path.

Removed functions: make_llm_completion/amake_llm_completion were never exported from __init__.py and no remaining references exist. No public API break.

Tests: The new dispatch tests (test_generate_dispatches_to_configured_api, test_agenerate_dispatches_to_configured_api), the real HTTP transport title test, and the agent step kwargs verification are meaningful and exercise real code paths. The live server test mock updates correctly separate title calls from agent calls.

Risk Assessment

MEDIUM — This PR changes the LLM dispatch path for agent steps, condensation, hook evaluation, cleanup, vision inspection, Ask Oracle, and title generation. While the refactoring is behavior-preserving by design and the changes are well-tested, any change to the agent LLM call path could plausibly affect benchmark/evaluation performance. Per the repo eval-risk policy, I am not approving and flagging this for a human maintainer to decide after running lightweight evals.

Recommendation: Run a lightweight eval (e.g., a small SWE-bench or GAIA subset) to confirm no regression before merging.

@neubig
neubig force-pushed the fix/subscription-title-generation branch from 4ae12e8 to 432cff4 Compare September 12, 2026 16:23
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: 432cff44fa233471e8b00d8e62655de97d518fc6
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/54c068f9-dcf0-4e4a-a79d-8950c7cc39a6

This comment was posted by an AI agent (OpenHands).

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Summary

The PR introduces LLM.generate()/LLM.agenerate() as a generic dispatch method, removes make_llm_completion/amake_llm_completion, and migrates all callers to direct generate()/agenerate() calls. The design is clean — API-mode selection lives in the LLM abstraction, agent-specific policy (add_security_risk_prediction=True, store=False, tools, callbacks) stays explicit at call sites. The title-generation transport test using a real HTTP server is excellent.

However, there is one material correctness bug in the telemetry changes, and the PR touches agent step logic, condenser behavior, and hook evaluation without eval evidence.

🔴 Critical Issue

telemetry.py: __dict__.get() breaks cache token accounting for Pydantic extra fields

The three getattr(usage, ..., 0) calls were replaced with usage.__dict__.get(..., 0) / p_details.__dict__.get(..., 0) to remove them from the forbidden dynamic attributes baseline. But litellm's Usage model uses extra="allow", so dynamically-set fields like cache_read_input_tokens and cache_creation_input_tokens are stored in __pydantic_extra__, not in __dict__. This means __dict__.get("cache_read_input_tokens", 0) always returns 0.

Verified empirically:

u = Usage.model_validate({
    'prompt_tokens': 1000, 'cache_read_input_tokens': 800, 'cache_creation_input_tokens': 200
})
getattr(u, 'cache_read_input_tokens', 0)  # → 800 (correct)
u.__dict__.get('cache_read_input_tokens', 0)  # → 0 (broken)

This silently zeroes out Anthropic prompt-caching token counts in telemetry/metrics for both cache reads and cache writes. The same issue affects p_details.__dict__.get("cache_creation_tokens", 0) when cache_creation_tokens is an extra field on the details model.

The _cache_creation_input_tokens change (using isinstance(usage, Usage) + direct attribute access) is fine — that field is a declared PrivateAttr on Usage, so direct access works and won't raise AttributeError.

Suggested fix: Use __pydantic_extra__ or keep getattr and accept the baseline entry. Alternatively, model_dump() would include extra fields but is heavier.

Eval Risk

This PR changes agent step dispatch (agent.py), condenser LLM calls (llm_summarizing_condenser.py), hook evaluation (executor.py), and cleanup profile behavior — all of which could plausibly affect benchmark/evaluation performance. No eval monitor link or human eval confirmation is provided in the PR description. Flagging for a human maintainer to decide after running lightweight evals.

Risk Assessment

  • Overall PR: 🟡 MEDIUM
  • The telemetry bug is a silent regression in cost/token accounting that won't crash but produces incorrect metrics.
  • The agent behavior changes are functionally equivalent to the previous make_llm_completion wrapper (same dispatch logic, same parameters), so the risk of behavioral regression is low — but eval validation is warranted given the scope.
  • The CI baseline-shrink guard and the stream_context.py try/except change are clean and correct.

Verdict

COMMENT — the telemetry __dict__.get bug should be fixed before merge, and eval validation is recommended given the scope of agent/condenser/hook changes.

Comment thread openhands-sdk/openhands/sdk/llm/utils/telemetry.py Outdated
@neubig
neubig force-pushed the fix/subscription-title-generation branch 2 times, most recently from 701cd8b to f19a071 Compare September 12, 2026 18:48
openhands+astra requested by @neubig

Created by an AI agent (OpenHands) on behalf of @neubig.

Co-authored-by: openhands <openhands@all-hands.dev>
Created by an AI agent (OpenHands) on behalf of @neubig.

Co-authored-by: openhands <openhands@all-hands.dev>
Remove agent completion wrappers, migrate auxiliary and agent callers to LLM.generate/agenerate, and preserve agent-only security policy at Agent.step/astep.

Created by an AI agent (OpenHands) on behalf of @neubig.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
neubig force-pushed the fix/subscription-title-generation branch 2 times, most recently from 5bb59cc to f19a071 Compare September 12, 2026 19:06
@neubig
neubig removed this pull request from stack #4972 September 12, 2026 19:09
@neubig
neubig changed the base branch from fix/async-secret-masking to main September 12, 2026 19:10
@neubig
neubig force-pushed the fix/subscription-title-generation branch from f19a071 to 5bb59cc Compare September 12, 2026 19:10
@neubig
neubig merged commit 76e9e25 into main Sep 12, 2026
96 checks passed
@neubig
neubig deleted the fix/subscription-title-generation branch September 12, 2026 19:22
@github-actions github-actions Bot mentioned this pull request Sep 15, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants